<?php
/**
 * Templum - php template engine.
 *
 * Features:
 *  - automatically create cache
 *  - automatically create helper objects if need
 *
 * @author Anton Danilchenko <anton.danilchenko@gmail.com>
 */
class Templum
{
	/**
	 * Template variables.
	 * This variables used only in template.
	 *
	 * @var array
	 */
	private $_aVariables = array();
	/**
	 * Helpers objects.
	 *
	 * @var array
	 */
	private $_aHelpers = array();
	/**
	 * Data to processing in helper.
	 *
	 * @var string
	 */
	private $_sHelperData = '';
	/**
	 * Path to helpers directory.
	 *
	 * @var string
	 */
	private $_sHelpersDir;
	/**
	 * Path to templates directory.
	 *
	 * @var string
	 */
	private $_sTemplateDir;
	/**
	 * Path to cache directory.
	 *
	 * @var string
	 */
	private $_sCacheDir;


	/**
	 * Set directory paths.
	 *
	 * @param string $sTemplateDir: Path to templates directory
	 * @param string $sCacheDir: Path to cache directory
	 * @return Templum
	 */
	public function __construct($sTemplateDir='template', $sCacheDir='cache', $sHelpersDir='Helper')
	{
		// check directory
		if ( ! is_dir($sHelpersDir) OR ! is_readable($sHelpersDir) )
			throw new Templum_Exception('Helpers directory not found or not readable');
		if ( ! is_dir($sTemplateDir) OR ! is_readable($sTemplateDir) )
			throw new Templum_Exception('Template directory should be created and readable');
		if ( ! is_dir($sCacheDir) OR ! is_readable($sCacheDir) OR ! is_writable($sCacheDir))
			throw new Templum_Exception('Cache directory should be created, readable and writable');
		// set directory
		$this->_sTemplateDir = $sTemplateDir;
		$this->_sCacheDir = $sCacheDir;
		$this->_sHelpersDir = $sHelpersDir;
	}


	/**
	 * Execute and return template.
	 *
	 * @param string $sName: path to template file (start from template dir)
	 * @var string
	 */
	public function getTemplate($sPath)
	{
		// renew cache
		if ( ! $this->_isCached($sPath) ) {
			// get fuul path to template
			$sTemplatePath = $this->_getTemplatePath($sPath);
			// read template file
			$sTemplateSource = implode('', file($sTemplatePath));
			// replace placeholders
			$sTemplateData = $this->_getTemplatePrepared($sTemplateSource);
			// save changed template data to cache
			$this->_setCache($sPath, $sTemplateData);
		}
		// collect printed data in buffer
		ob_start();
		// execute cache
		$this->_getCache($sPath);
		// save collected data in variable
		$sProcessedTemplate = ob_get_contents();
		// stop collect data
		ob_end_clean();
		// return collected data
		return $sProcessedTemplate;
	}


	/**
	 * Clear all cache files.
	 *
	 * @return bool
	 */
	public function clearCache()
	{
		self::_rmDir($this->_sCacheDir, true, false);
	}


	/**
	 * Set value in variable.
	 *
	 * @param string $sName: variable name
	 * @param string $mValue: variable value
	 */
	public function __set($sName, $mValue)
	{
		$this->_aVariables[$sName] = $mValue;
	}


	/**
	 * Get helper object (without parameters).
	 *
	 * @param string $sName: helper name
	 */
	public function __get($sName)
	{
		// call helper without parameters
		return $this->__call($sName, array());
	}


	/**
	 * Get helper object (with parameters).
	 *
	 * @param string $sName: helper name
	 * @param string $aParams: helper parameters
	 */
	public function __call($sName, $aParams)
	{
		// correct helper name
		$sName = ucfirst(strtolower($sName));
		// create new helper
		if ( ! isset($this->_aHelpers[$sName]) ) {
			// set path to helper file
			$sHelperFile = $this->_getHelperPath("{$sName}.php");
			// check helper file
			if ( ! file_exists($sHelperFile) )
				throw new Templum_Exception("Helper with name '{$sName}' not defined");
			// include file
			require_once ("{$this->_sHelpersDir}/Interface.php");
			require_once $sHelperFile;
			// define helren full name
			$sHelperName = "Templum_Helper_{$sName}";
			// check file interface
			if ( ! class_exists($sHelperName) OR ! in_array('Templum_Helper_Interface', class_implements($sHelperName)) )
				throw new Templum_Exception("Helper with name '{$sHelperName}' not defined or not implement of interface Templum_Helper_Interface");
			// create helper
			$this->_aHelpers[$sName] = new $sHelperName;
		}
		// processing data in helper
		$this->_aHelpers[$sName]->handle($this->_sHelperData, $aParams);
		// return itself (for OOP calls)
		return $this;
	}


	/**
	 * Display helper handled data.
	 *
	 * @return string
	 */
	public function __toString()
	{
		return $this->_sHelperData;
	}


	/**
	 * Check if cache actual.
	 *
	 * @param string $sPath: path to template file (start from template dir)
	 * @return bool
	 */
	private function _isCached($sPath)
	{
		// full path to template and cache files
		$sCachePath = $this->_getCachePath($sPath);
		$sTemplatePath = $this->_getTemplatePath($sPath);
		// cache newest
		if( file_exists($sTemplatePath) AND file_exists($sCachePath) AND filemtime($sTemplatePath) == filemtime($sCachePath) )
			return true;
		// cache old
		return false;
	}



	/**
	 * Return cache data.
	 *
	 * @param string $sPath: path to template file (start from template dir)
	 * @return string
	 */
	private function _getCache($sPath)
	{
		// full path to template
		$sCachePath = $this->_getCachePath($sPath);
		// получаем кэшированные данные
		if( ! $this->_isCached($sPath) )
			throw new Templum_Exception("Cache file not found. Path: {$sCachePath}");
		// insert variables to view in template
		extract($this->_aVariables, EXTR_REFS | EXTR_SKIP);
		// return cache data
		return (include $sCachePath);
	}


	/**
	 * Save cache file.
	 *
	 * @param string $sPath: path to template file (start from template dir)
	 * @param string $sData: saved data to cache
	 * @return bool
	 */
	private function _setCache($sPath, $sData)
	{
		// full path to template
		$sCachePath = $this->_getCachePath($sPath);
		$sTemplatePath = $this->_getTemplatePath($sPath);
		// create directory hierarhy
		$sDirs = dirname($sCachePath);
		if ( ! is_dir($sDirs) )
			mkdir($sDirs, 0755, true);
		// save cache
		$oFile = fopen($sCachePath, 'w');
		fwrite($oFile, $sData);
		fclose($oFile);
		// set cache modification time
		$iDate = filemtime($sTemplatePath);
		touch($sCachePath, $iDate);
	}


	/**
	 * Return full path to template.
	 *
	 * @param string $sPath: path to template file (start from template dir)
	 */
	private function _getTemplatePath($sPath)
	{
		// delete start slash (or backslash)
		$sPath = ltrim($sPath, '/\\');
		// full path to template file
		return "{$this->_sTemplateDir}/{$sPath}";
	}


	/**
	 * Return full path to cache file.
	 *
	 * @param string $sPath: path to template file (start from template dir)
	 */
	private function _getCachePath($sPath)
	{
		// delete start slash (or backslash)
		$sPath = ltrim($sPath, '/\\');
		// full path to cache file
		return "{$this->_sCacheDir}/{$sPath}";
	}


	/**
	 * Return full path to cache file.
	 *
	 * @param string $sPath: path to helper file (start from helpers dir)
	 */
	private function _getHelperPath($sPath)
	{
		// delete start slash (or backslash)
		$sPath = ltrim($sPath, '/\\');
		// full path to cache file
		return "{$this->_sHelpersDir}/{$sPath}";
	}


	/**
	 * Replace placeholders in template and return result.
	 *
	 * @param string $sTemplateSource: raw template code
	 */
	private function _getTemplatePrepared($sTemplateSource)
	{
		// check template syntax
		if (preg_match('`(<\?|\?>)`', $sTemplateSource))
			throw new Templum_Exception('In template code should not be php tags (<?, ?>)');
		// block elements
		$sPlaceholders = 'if';
		$sTemplateSource = preg_replace("`<(({$sPlaceholders}) +\([^>]+\)) *>`", '<?php $1 { ?>', $sTemplateSource);
		$sPlaceholders = 'foreach';
		$sTemplateSource = preg_replace("`<(({$sPlaceholders}) +\( *([^ ]+)[^>]+\)) *>`", '<?php if(!empty($3)) $1 { ?>', $sTemplateSource);
		$sPlaceholders = 'foreach|if';
		$sTemplateSource = preg_replace("`</({$sPlaceholders})>`", '<?php } ?>', $sTemplateSource);
		// one-line elements
		$sPlaceholders = 'elseif';
		$sTemplateSource = preg_replace("`<(({$sPlaceholders}) +\([^>]+\)) */>`", '<?php } $1 { ?>', $sTemplateSource);
		$sPlaceholders = 'else';
		$sTemplateSource = preg_replace("`<({$sPlaceholders})/>`", '<?php } $1 { ?>', $sTemplateSource);
		$sPlaceholders = '\$';
		$sTemplateSource = preg_replace("`<(({$sPlaceholders}).+)/>`U", '<?php echo $1; ?>', $sTemplateSource);
		$sPlaceholders = 'include';
		$sTemplateSource = preg_replace("`<(({$sPlaceholders}) +'([^'\"]+)') */>`U", '<?php echo $this->getTemplate("$3"); ?>', $sTemplateSource);
		// helpers
		$sPlaceholders = 'helper';
		$sTemplateSource = preg_replace("`<({$sPlaceholders}) +\(([^)]+)\)(.*)/>`U", '<?php $this->_sHelperData = $2; echo \$this$3; ?>', $sTemplateSource);
		$sTemplateSource = preg_replace("`<({$sPlaceholders}) +\(\)(.*)/>`U", '<?php $this->_sHelperData = ""; echo \$this$2; ?>', $sTemplateSource);
		// return result
		return $sTemplateSource;
	}


	/**
	 * Delete selected directory and all files in this directory.
	 *
	 * @param string $sPath: full path to directory
	 * @param bool $bResursive: delete if dir not empty
	 * @param bool $bDeleteSourceDir: delete current directory or all content
	 */
	private static function _rmDir($sPath, $bResursive=false, $bDeleteSourceDir=true)
	{
	    $sPath = rtrim($sPath, '/') . '/';
	    $oHandle = opendir($sPath);
	    while (false !== ($sFile = readdir($oHandle))) {
	        if($sFile == "." OR $sFile == ".." )
	        	continue;
	        // set fullpath to file
            $sFullpath = $sPath . $sFile;
            if ( is_dir($sFullpath) )
                self::_rmDir($sFullpath, $bResursive, true);
            else
                unlink($sFullpath);
	    }
	    // close current dir
	    closedir($oHandle);
	    // delete current dir
	    if ($bDeleteSourceDir)
	    	rmdir($sPath);
	}
}

class Templum_Exception extends Exception
{
}